import {
  BufferGeometry,
  Color,
  FileLoader,
  Float32BufferAttribute,
  Group,
  Loader,
  LoaderUtils,
  Mesh,
  MeshPhongMaterial,
} from 'three'
import { unzipSync } from 'fflate'
import { decodeText } from '../_polyfill/LoaderUtils'

/**
 * Description: Early release of an AMF Loader following the pattern of the
 * example loaders in the three.js project.
 *
 * Usage:
 *	const loader = new AMFLoader();
 *	loader.load('/path/to/project.amf', function(objecttree) {
 *		scene.add(objecttree);
 *	});
 *
 * Materials now supported, material colors supported
 * Zip support, requires fflate
 * No constellation support (yet)!
 *
 */

class AMFLoader extends Loader {
  constructor(manager) {
    super(manager)
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this

    const loader = new FileLoader(scope.manager)
    loader.setPath(scope.path)
    loader.setResponseType('arraybuffer')
    loader.setRequestHeader(scope.requestHeader)
    loader.setWithCredentials(scope.withCredentials)
    loader.load(
      url,
      function (text) {
        try {
          onLoad(scope.parse(text))
        } catch (e) {
          if (onError) {
            onError(e)
          } else {
            console.error(e)
          }

          scope.manager.itemError(url)
        }
      },
      onProgress,
      onError,
    )
  }

  parse(data) {
    function loadDocument(data) {
      let view = new DataView(data)
      const magic = String.fromCharCode(view.getUint8(0), view.getUint8(1))

      if (magic === 'PK') {
        let zip = null
        let file = null

        console.log('THREE.AMFLoader: Loading Zip')

        try {
          zip = unzipSync(new Uint8Array(data))
        } catch (e) {
          if (e instanceof ReferenceError) {
            console.log('THREE.AMFLoader: fflate missing and file is compressed.')
            return null
          }
        }

        for (file in zip) {
          if (file.toLowerCase().substr(-4) === '.amf') {
            break
          }
        }

        console.log('THREE.AMFLoader: Trying to load file asset: ' + file)
        view = new DataView(zip[file].buffer)
      }

      const fileText = decodeText(view)
      const xmlData = new DOMParser().parseFromString(fileText, 'application/xml')

      if (xmlData.documentElement.nodeName.toLowerCase() !== 'amf') {
        console.log('THREE.AMFLoader: Error loading AMF - no AMF document found.')
        return null
      }

      return xmlData
    }

    function loadDocumentScale(node) {
      let scale = 1.0
      let unit = 'millimeter'

      if (node.documentElement.attributes.unit !== undefined) {
        unit = node.documentElement.attributes.unit.value.toLowerCase()
      }

      const scaleUnits = {
        millimeter: 1.0,
        inch: 25.4,
        feet: 304.8,
        meter: 1000.0,
        micron: 0.001,
      }

      if (scaleUnits[unit] !== undefined) {
        scale = scaleUnits[unit]
      }

      console.log('THREE.AMFLoader: Unit scale: ' + scale)
      return scale
    }

    function loadMaterials(node) {
      let matName = 'AMF Material'
      const matId = node.attributes.id.textContent
      let color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }

      let loadedMaterial = null

      for (let i = 0; i < node.childNodes.length; i++) {
        const matChildEl = node.childNodes[i]

        if (matChildEl.nodeName === 'metadata' && matChildEl.attributes.type !== undefined) {
          if (matChildEl.attributes.type.value === 'name') {
            matName = matChildEl.textContent
          }
        } else if (matChildEl.nodeName === 'color') {
          color = loadColor(matChildEl)
        }
      }

      loadedMaterial = new MeshPhongMaterial({
        flatShading: true,
        color: new Color(color.r, color.g, color.b),
        name: matName,
      })

      if (color.a !== 1.0) {
        loadedMaterial.transparent = true
        loadedMaterial.opacity = color.a
      }

      return { id: matId, material: loadedMaterial }
    }

    function loadColor(node) {
      const color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }

      for (let i = 0; i < node.childNodes.length; i++) {
        const matColor = node.childNodes[i]

        if (matColor.nodeName === 'r') {
          color.r = matColor.textContent
        } else if (matColor.nodeName === 'g') {
          color.g = matColor.textContent
        } else if (matColor.nodeName === 'b') {
          color.b = matColor.textContent
        } else if (matColor.nodeName === 'a') {
          color.a = matColor.textContent
        }
      }

      return color
    }

    function loadMeshVolume(node) {
      const volume = { name: '', triangles: [], materialid: null }

      let currVolumeNode = node.firstElementChild

      if (node.attributes.materialid !== undefined) {
        volume.materialId = node.attributes.materialid.nodeValue
      }

      while (currVolumeNode) {
        if (currVolumeNode.nodeName === 'metadata') {
          if (currVolumeNode.attributes.type !== undefined) {
            if (currVolumeNode.attributes.type.value === 'name') {
              volume.name = currVolumeNode.textContent
            }
          }
        } else if (currVolumeNode.nodeName === 'triangle') {
          const v1 = currVolumeNode.getElementsByTagName('v1')[0].textContent
          const v2 = currVolumeNode.getElementsByTagName('v2')[0].textContent
          const v3 = currVolumeNode.getElementsByTagName('v3')[0].textContent

          volume.triangles.push(v1, v2, v3)
        }

        currVolumeNode = currVolumeNode.nextElementSibling
      }

      return volume
    }

    function loadMeshVertices(node) {
      const vertArray = []
      const normalArray = []
      let currVerticesNode = node.firstElementChild

      while (currVerticesNode) {
        if (currVerticesNode.nodeName === 'vertex') {
          let vNode = currVerticesNode.firstElementChild

          while (vNode) {
            if (vNode.nodeName === 'coordinates') {
              const x = vNode.getElementsByTagName('x')[0].textContent
              const y = vNode.getElementsByTagName('y')[0].textContent
              const z = vNode.getElementsByTagName('z')[0].textContent

              vertArray.push(x, y, z)
            } else if (vNode.nodeName === 'normal') {
              const nx = vNode.getElementsByTagName('nx')[0].textContent
              const ny = vNode.getElementsByTagName('ny')[0].textContent
              const nz = vNode.getElementsByTagName('nz')[0].textContent

              normalArray.push(nx, ny, nz)
            }

            vNode = vNode.nextElementSibling
          }
        }

        currVerticesNode = currVerticesNode.nextElementSibling
      }

      return { vertices: vertArray, normals: normalArray }
    }

    function loadObject(node) {
      const objId = node.attributes.id.textContent
      const loadedObject = { name: 'amfobject', meshes: [] }
      let currColor = null
      let currObjNode = node.firstElementChild

      while (currObjNode) {
        if (currObjNode.nodeName === 'metadata') {
          if (currObjNode.attributes.type !== undefined) {
            if (currObjNode.attributes.type.value === 'name') {
              loadedObject.name = currObjNode.textContent
            }
          }
        } else if (currObjNode.nodeName === 'color') {
          currColor = loadColor(currObjNode)
        } else if (currObjNode.nodeName === 'mesh') {
          let currMeshNode = currObjNode.firstElementChild
          const mesh = { vertices: [], normals: [], volumes: [], color: currColor }

          while (currMeshNode) {
            if (currMeshNode.nodeName === 'vertices') {
              const loadedVertices = loadMeshVertices(currMeshNode)

              mesh.normals = mesh.normals.concat(loadedVertices.normals)
              mesh.vertices = mesh.vertices.concat(loadedVertices.vertices)
            } else if (currMeshNode.nodeName === 'volume') {
              mesh.volumes.push(loadMeshVolume(currMeshNode))
            }

            currMeshNode = currMeshNode.nextElementSibling
          }

          loadedObject.meshes.push(mesh)
        }

        currObjNode = currObjNode.nextElementSibling
      }

      return { id: objId, obj: loadedObject }
    }

    const xmlData = loadDocument(data)
    let amfName = ''
    let amfAuthor = ''
    const amfScale = loadDocumentScale(xmlData)
    const amfMaterials = {}
    const amfObjects = {}
    const childNodes = xmlData.documentElement.childNodes

    let i, j

    for (i = 0; i < childNodes.length; i++) {
      const child = childNodes[i]

      if (child.nodeName === 'metadata') {
        if (child.attributes.type !== undefined) {
          if (child.attributes.type.value === 'name') {
            amfName = child.textContent
          } else if (child.attributes.type.value === 'author') {
            amfAuthor = child.textContent
          }
        }
      } else if (child.nodeName === 'material') {
        const loadedMaterial = loadMaterials(child)

        amfMaterials[loadedMaterial.id] = loadedMaterial.material
      } else if (child.nodeName === 'object') {
        const loadedObject = loadObject(child)

        amfObjects[loadedObject.id] = loadedObject.obj
      }
    }

    const sceneObject = new Group()
    const defaultMaterial = new MeshPhongMaterial({ color: 0xaaaaff, flatShading: true })

    sceneObject.name = amfName
    sceneObject.userData.author = amfAuthor
    sceneObject.userData.loader = 'AMF'

    for (const id in amfObjects) {
      const part = amfObjects[id]
      const meshes = part.meshes
      const newObject = new Group()
      newObject.name = part.name || ''

      for (i = 0; i < meshes.length; i++) {
        let objDefaultMaterial = defaultMaterial
        const mesh = meshes[i]
        const vertices = new Float32BufferAttribute(mesh.vertices, 3)
        let normals = null

        if (mesh.normals.length) {
          normals = new Float32BufferAttribute(mesh.normals, 3)
        }

        if (mesh.color) {
          const color = mesh.color

          objDefaultMaterial = defaultMaterial.clone()
          objDefaultMaterial.color = new Color(color.r, color.g, color.b)

          if (color.a !== 1.0) {
            objDefaultMaterial.transparent = true
            objDefaultMaterial.opacity = color.a
          }
        }

        const volumes = mesh.volumes

        for (j = 0; j < volumes.length; j++) {
          const volume = volumes[j]
          const newGeometry = new BufferGeometry()
          let material = objDefaultMaterial

          newGeometry.setIndex(volume.triangles)
          newGeometry.setAttribute('position', vertices.clone())

          if (normals) {
            newGeometry.setAttribute('normal', normals.clone())
          }

          if (amfMaterials[volume.materialId] !== undefined) {
            material = amfMaterials[volume.materialId]
          }

          newGeometry.scale(amfScale, amfScale, amfScale)
          newObject.add(new Mesh(newGeometry, material.clone()))
        }
      }

      sceneObject.add(newObject)
    }

    return sceneObject
  }
}

export { AMFLoader }
